compiler,runtime: call fcntl through a C wrapper on darwin - #5632
Conversation
The third parameter of fcntl is variadic, and on darwin/arm64 a variadic argument goes on the stack and not in a register. A call to libc fcntl through a plain three-argument function pointer thus makes the callee read that argument from an unrelated stack slot. open() already has a C wrapper for the same reason. The symptom is quiet. fcntl(fd, F_SETFD, FD_CLOEXEC) sets the flag or does not, which depends on the stack contents, so the result is the same for one binary and different between binaries. syscall.CloseOnExec is the main caller, so when it fails, every descriptor of the program goes into every process that it starts. A child that holds a copy of the write end of a pipe keeps that pipe from a report of EOF, which is how os/exec collects the output of a command. Measured on macOS 26.6 arm64 before this change, fcntl(fd, F_DUPFD, 100) returns EINVAL, and three F_SETFL calls with 0x4, 0x0 and 0x8 all leave F_GETFL with 0x48. The wrapper takes the argument as a uintptr_t so that the pointer commands reached through syscall.fcntlPtr use it too. Both spellings go through libc_fcntl_trampoline, and on a little-endian target the int commands read the low half of the same stack slot. The new tests in src/os cover both shapes. TestFcntlSetNonblock fails on darwin before this change and passes after it.
c3bdc58 to
9161a8c
Compare
|
Rebased on dev after the 0.42.0 release. The problem is present in v0.42.0 as |
|
Closing this duplicate in favor of the earlier, broader #5612. That PR routes both the stdlib and x/sys Darwin trampoline paths through fixed-signature wrappers. This PR only fixes the stdlib fcntl path. Its integer and pointer argument regression tests remain available in commit 9161a8c, and I have offered them on #5612. The process PR #5634 will reference #5612 as its Darwin prerequisite. |
Status: closed in favor of the earlier and broader #5612. The tests remain available for that PR. The evidence below is historical.
compiler,runtime: call fcntl through a C wrapper on darwin
What this does
The third parameter of
fcntlis variadic, and on darwin/arm64 a variadicargument goes on the stack and not in a register. A call to libc
fcntlthrougha plain three-argument function pointer thus makes the callee read that argument
from an unrelated stack slot.
open()already has a C wrapper insrc/runtime/os_darwin.cfor the same reason, and this adds the matching onefor
fcntl.The wrapper takes the argument as a
uintptr_tso that the pointer commandsreached through
syscall.fcntlPtruse it too. Both spellings go throughlibc_fcntl_trampoline, and on a little-endian target the int commands read thelow half of the same stack slot.
Why it matters
The failure is quiet.
fcntl(fd, F_SETFD, FD_CLOEXEC)sets the flag or doesnot, which depends on the stack contents, so the result is the same for one
binary and different between binaries.
syscall.CloseOnExecis the main caller,so when it fails, every descriptor of the program goes into every process that
it starts, and a child that holds a copy of the write end of a pipe keeps that
pipe from a report of EOF.
Evidence
Measured on macOS 26.6 arm64 before this change.
fcntl(fd, F_DUPFD, 100)returns EINVAL.F_SETFLcalls with 0x4, 0x0 and 0x8 all leaveF_GETFLreading 0x48.Two tests are added to
src/os/fcntl_test.go. Theospackage is already inTEST_PACKAGES_FAST, so both run in the linux and the macOS CI jobs with nomakefile change.
TestFcntlSetNonblockF_SETFLround trip. A read on an empty non-blocking pipe must return EAGAIN. It fails on darwin before this change.TestFcntlGetLockF_GETLKon an unlocked file must reportF_UNLCK.Both pass on macOS 26.6 arm64 with this change. Linux is unaffected, because it
does not use the darwin trampoline path, and the tests are a regression guard
there.
A downstream product also ships binaries built with a fork that carries this
change, in the published release dispat v1.4.0.
https://github.com/yohimik/dispat/releases/tag/services%2Fdispat%2Fv1.4.0
Scope
compiler/syscall.goonly changescreateDarwinFuncPCABI0Call, so no othertarget sees a difference.
Related
This is a prerequisite for correct process creation on darwin.
os.Pipeondarwin has no
pipe2, so it must mark both ends close-on-exec withfcntl.Related pull requests
Each open PR in this series has a separate change. A dependency is not a copied commit.
In tinygo-org/net
Full Darwin networking also needs the merged net changes and a later src/net pin update. No upstream merge or current full-suite pass is implied by this list.